package builder;

import builder.construction.*;
import builder.database.Column;
import builder.database.Table;
import util.StringUtils;

import java.util.*;
import java.util.Map.Entry;

/**
 * @author Alex
 */
public class QueryBuilder 
	implements 	Builder, 		EndBuilder, 	SelectBuilder, 
				FromBuilder, 	WhereBuilder, 	InsertBuilder, 	
				ValuesBuilder,	UpdateBuilder,	SetBuilder,
				DeleteBuilder{
		
	private List<String> 		selects;
	private String		        from;
	private Table 				table;
	private String		        columnNames;
	private String		        values;
	private List<String> 		joins;
	private List<String>		conditions;
	private List<String>		sortings;
	private Map<Column, String>	updateSets;
	
	private Column				currentColumn;
	private boolean				distinct = false;
	private QueryType			type;
	
	private QueryBuilder(QueryType type) {
		this.type 	= type;
		selects 	= new LinkedList<String>();

		joins 		= new LinkedList<String>();
		conditions 	= new LinkedList<String>();
		sortings	= new LinkedList<String>();
		updateSets	= new LinkedHashMap<Column, String>();
	}
	
	public static SelectBuilder createSelectQuery() {
		return new QueryBuilder(QueryType.SELECT);
	}
	
	public static InsertBuilder createInsertQuery() {
		return new QueryBuilder(QueryType.INSERT);
	}
	
	public static UpdateBuilder createUpdateQuery() { 
		return new QueryBuilder(QueryType.UPDATE);
	}
	
	public static DeleteBuilder createDeleteQuery() {
		return new QueryBuilder(QueryType.DELETE);
	}
	
	/* --- SELECTBUILDER IMPLEMENTATION --- */
	
	public FromBuilder select(Column ... columns) {	
		selects.add(StringUtils.join(getFullColumnNames(columns), ","));
		return this;
	}
	
	public FromBuilder selectDistinct(Column ... columns) {
		distinct = true;
		return select(columns);
	}
	
	public FromBuilder selectAll() {
		selects.clear();
        selects.add("*");
		return this;
	}
	
	/* --- FROMBUILDER IMPLEMENTATION --- */
	
	public Builder from(Table table) {
		from = table.getTableName() + " " + table.getTableShortName();
		return this;
	}
	
	/* --- INSERTBUILDER IMPLEMENTATION --- */
	
	@Override
	public ValuesBuilder insert(Table table, Column... columns) {
		this.table = table;
		columnNames = StringUtils.join(getColumnNames(columns), ",");
		return this;
	}
	
	/* --- VALUESBUILDER IMPLEMENTATION --- */
	
	@Override
	public Builder values(String... values) {
        for(int i = 0; i < values.length; i++)
            values[i] = parseString(values[i]);
		this.values = StringUtils.join(Arrays.asList(values), ",");
		return this;
	}
	
	/* --- UPDATEBUILDER IMPLEMENTATION --- */
	
	@Override
	public SetBuilder update(Table table) {
		this.table = table;
		return this;
	}
	
	/* --- SETBUILDER IMPLEMENTATION --- */
	
	@Override
	public SetBuilder set(Column column, String value) {
		if(column.getTable() != table)
			throw new IllegalArgumentException("You cannot set values of columns of a different table." +
					" Specified: " + column.getTable().getTableName() +
					" Expected: " + table.getTableName());
        if(updateSets.containsKey(column))
            throw new IllegalArgumentException("You cannot set the value of a column twice: " +
                    column.getFullColumnName());
		updateSets.put(column, value);
		return this;
	}
	
	/* --- DELETEBUILDER IMPLEMENTATION --- */
	@Override
	public Builder delete(Table table) {
		this.table = table;
		return this;
	}
	
	/* --- BUILDER IMPLEMENTATION --- */
		
	public Builder join(Table table, Column columnLeft, Column columnRight) {			
		joins.add("JOIN " + table.getTableName() + " " + table.getTableShortName()
                + " ON " + columnLeft.getFullColumnName() + " = " + columnRight.getFullColumnName());
        return this;
	}

	public WhereBuilder where(Column column) {
		currentColumn = column;
		return this;
	}
	
	public Builder orderByAscending(Column...columns) {
        for(Column column: columns)
            sortings.add(column.getFullColumnName() + " ASC");
		return this;		
	}

	public Builder orderByDescending(Column...columns) {
        for(Column column: columns)
            sortings.add(column.getFullColumnName() + " DESC");
        return this;
	}
	
	/* --- ENDBUILDER IMPLEMENTATION --- */
	
	public String build() {
		switch(type) {
			case SELECT: 	return buildSelectQuery();
			case INSERT: 	return buildInsertQuery();
			case UPDATE:	return buildUpdateQuery();
			case DELETE:	return buildDeleteQuery();
			default: throw new IllegalArgumentException("Error in QueryBuilder: " +
                    "query type not recognized + (" + type + ")");
		}
	}
	
	private String buildSelectQuery() {
		StringBuilder query = new StringBuilder(256);
		query.append("SELECT ").append((distinct?"DISTINCT ":"")).append(StringUtils.join(selects,","));
		query.append(" FROM ").append(from).append(" ");
		query.append(StringUtils.join(joins, ' '));
		if(!conditions.isEmpty())
			query.append(" WHERE ").append(StringUtils.join(conditions, " AND "));
		if(!sortings.isEmpty())
			query.append(" ORDER BY ").append(StringUtils.join(sortings, ','));
		return query.toString();
	}
	
	private String buildInsertQuery() {
        StringBuilder query = new StringBuilder(256)
                .append("INSERT INTO ").append(table.getTableName());
		query.append(" (").append(columnNames).append(")");
		query.append(" VALUES (").append(values).append(")");
		return query.toString();
	}
	
	private String buildUpdateQuery() {
		List<String> sets = new LinkedList<String>();
		StringBuilder query = new StringBuilder(256).append("UPDATE ").append(table.getTableName());
		query.append(" SET ");
		for(Entry<Column, String> set: updateSets.entrySet())
			sets.add(set.getKey().getColumnName() + "=" + parseString(set.getValue()));
		query.append(StringUtils.join(sets, ","));
		if(!conditions.isEmpty())
			query.append(" WHERE ").append(StringUtils.join(conditions, " AND "));
		return query.toString();
	}
	
	private String buildDeleteQuery() {
		StringBuilder query = new StringBuilder(256).append("DELETE FROM ").append(table.getTableName());
		if(!conditions.isEmpty())
			query.append(" WHERE ").append(StringUtils.join(conditions, " AND "));
		return query.toString();
	}
	

	/* --- WHEREBUILDER IMPLEMENTATION --- */
	
	public Builder equals(String value) {
		conditions.add(currentColumn.getFullColumnName() + " = " + parseString(value));
		return this;
	}
	

	public Builder notEquals(String value) {
		conditions.add(currentColumn.getFullColumnName() + " <> " + parseString(value));
		return this;
	}
	

	public Builder greaterThan(String value) {
		conditions.add(currentColumn.getFullColumnName() + " > " + parseString(value));
		return this;
	}
	

	public Builder lessThan(String value) {
		conditions.add(currentColumn.getFullColumnName() + " < " + parseString(value));
		return this;
	}
	

	public Builder greaterThanOrEquals(String value) {
		conditions.add(currentColumn.getFullColumnName() + " >= " + parseString(value));
		return this;
	}
	

	public Builder lessThanOrEquals(String value) {
		conditions.add(currentColumn.getFullColumnName() + " <= " + parseString(value));
		return this;
	}
	

	public Builder between(String lowerBound, String upperBound) {
		conditions.add(currentColumn.getFullColumnName() 
				+ " BETWEEN " + parseString(lowerBound) + " and " + parseString(upperBound) + "");
		return this;
	}

	public Builder like(String value) {
		conditions.add(currentColumn.getFullColumnName() + " LIKE " + parseString(value) + "");
		return this;
	}
	
	public Builder in(String... values) {
		List<String> parsedValues = new LinkedList<String>();
		for(String value: values)
			parsedValues.add(parseString(value));
		conditions.add(currentColumn.getFullColumnName() 
				+ " IN (" + StringUtils.join(parsedValues, ',') + ")");
		return this;
	}
	
	/* --- PRIVATE HELPER METHODS --- */
	
	private String parseString(String value) {
		if(StringUtils.isNumeric(value))
			return value;
		else
			return "'" + value + "'";
	}

	private List<String> getFullColumnNames(Column[] columns) {
		String[] fullColumnNames = new String[columns.length];
		for (int i = 0; i < columns.length; i++)
			fullColumnNames[i] = columns[i].getFullColumnName();
		return Arrays.asList(fullColumnNames);
	}

    private List<String> getColumnNames(Column[] columns) {
        String[] fullColumnNames = new String[columns.length];
        for (int i = 0; i < columns.length; i++)
            fullColumnNames[i] = columns[i].getColumnName();
        return Arrays.asList(fullColumnNames);
    }
}

